package com.cfw.authority;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.cfw.properties.CustomProperties;
import com.cfw.util.StringUtil;
import com.cfw.util.XmlUtil;
import com.cfw.xml.XmlManager;

/**
 * 접근 URL패턴별로 사용자권한정의 XML화일에 정의된 정보와 현재 접근URL을 비교하여, 수행할 수 있는 권한을 반환
 * 
 * 
 * <br/><b>History</b><br/>
 * <pre>
 * 2012. 2. 10. 최초작성
 * </pre>
 * @author 박형일
 * @version 1.0
 */
public class AuthorityManager {
	private static final String SID_XPATH =	"/auth-defs/auth[(boolean('{par}')=false() and boolean(par)=false()) or (boolean(par) and par='{par}')]/urls[url='{sid}']/..";
	private static final String DEFAULT_JOBTYPE_XPATH = "/auth-defs/default_job_types/job_type/text()";
	private static final Logger logger = LoggerFactory.getLogger(AuthorityManager.class);	
	private static final XmlManager xmlManager = XmlManager.getInstance();
	private final String xmlId;
	 
	/**
	 * singleton에 이용하기 위한 holder class 선언
	 *		private static final class AuthorityManagerHolder { static final AuthorityManager authorityManager = new AuthorityManager(); }
	 */
	private static AuthorityManager instance = new AuthorityManager();

	public static AuthorityManager getInstance() { return instance; }
	
	public static abstract class AbstractValidateMethodHolder {
		abstract protected boolean run();
	}
	
	/**
	 * singleton instance 생성시 권한정의xml화일에 대한 로딩 
	 */
	private AuthorityManager() { 
		try {
			final String resourceName =	getResourceName();
			
			xmlId = xmlManager.loadXmlStream(resourceName);
		} catch (ParserConfigurationException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		} catch (SAXException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 권한정의 XML 화일경로는 property에서 얻어온다. 
	 * @return
	 */
	private String getResourceName() {
		final String xmlName = CustomProperties.getProperty("resourcename.auth");
		
		logger.debug("*** xmlName:" + xmlName);
		
		if (StringUtils.isEmpty(xmlName)) { throw new RuntimeException("XML 구분자가 비어 있습니다."); }

		return xmlName;
	}
		
		/**
		 * "R", "D", "I", "U" 문자열을 담은 set을 반환한다.
		 * (모든 method에 대한 권한을 갖는다는 의미이다.)
		 * @return
		 */
		public Set<String> getDefaultMethod() throws XPathException {
			final Document document = xmlManager.getDocuments(xmlId);
			
			if (document == null) { throw new IllegalStateException("XML 도큐먼트가 NULL 입니다."); }
			
			final XPath xpath = XPathFactory.newInstance().newXPath();
			final List<String> methods = XmlUtil.getXPathNodeValues(xpath, DEFAULT_JOBTYPE_XPATH, document);
			
			HashSet<String> methodSet = null;
			
			if (methods != null) { 
				methodSet = new HashSet<String>(methods); 
			} else {
				methodSet = new HashSet<String>();
			}
			
			logger.debug("********************************************************************************");
			logger.debug("AuthorityManager.getDefaultMethod");
			logger.debug("*** set:" + methodSet);
			logger.debug("********************************************************************************");
			
			return methodSet;
		}

		/**
		 * 수행가능한 job type(R,D,I,U) 반환, 수행가능한 job type이 없을 경우, 빈 Set반환
		 * @param tree: URL, par가  매칭된 auth node 하위 node로 group_type, groups, auth_type, job_types node를 가지고 있다.     
		 * @param roleName: role name string array
		 * @param groupName: group name string array
		 * @param hasSession: session을 가지고 있는지 여부
		 * @return
		 */
		private Set<String> getMethod(final Node node, final String[] roleName, final String[] groupName, final boolean hasSession, final String remoteIp) throws XPathExpressionException {
			final XPath xpath = XPathFactory.newInstance().newXPath();
			
			final String tokGroupType = XmlUtil.getXPathNodeValue(xpath, "./group_type/text()", node);
			final List<String> groups = XmlUtil.getXPathNodeValues(xpath, "./groups/group/text()", node);
			final String authType = XmlUtil.getXPathNodeValue(xpath, "./auth_type/text()", node);
			final List<String> allowIps = XmlUtil.getXPathNodeValues(xpath, "./allow_ips/allow_ip/text()", node);
			
			String[] parGroupName =	null;
			if ("ROLE".equals(tokGroupType)) {
				parGroupName = roleName;
			} else
			if ("USER".equals(tokGroupType)) {
				parGroupName = groupName;
			}
			
			boolean isIncGroup = StringUtil.hasMatchOne(parGroupName, groups.toArray(new String[groups.size()])); // 포함되었는지 여부
			boolean isAllowedIp = (allowIps.size() == 0 || (allowIps.size() > 0 && allowIps.contains(remoteIp)));
			
			logger.debug("authType:" + authType + ", hasSession:" + hasSession + ", clientIP:" + remoteIp + ", isAllowedIp:" + isAllowedIp);
			
			HashSet<String> methodSet = new HashSet<String>();
			if (
			    isAllowedIp &&											// IP제한이 있다면 제한된 IP이어야 하며
				(
				("SESSION".equals(authType) && hasSession) || 			// auth type이 "SESSION"이고 session이 존재하는 경우
				("INC".equals(authType) && isIncGroup && hasSession) ||	// auth type이 "INC"이고 group name parameter 중에 권한정의 group name이 존재할 경우	
				("EXC".equals(authType) && !isIncGroup && hasSession)	// auth type이 "EXC"이고 group name parameter 중에 권한정의 group name이 존재하지 않을 경우
				)
			   ) {
				final List<String> jobTypes = XmlUtil.getXPathNodeValues(xpath, "./job_types/job_type/text()", node);
				
				if (jobTypes != null) { methodSet.addAll(jobTypes); }
			}   
			
			return methodSet;
		}		
		
	/**
	 * 권한정의화일의 url노드들을 찾아서 반환
	 * @param url
	 * @return
	 * @throws XPathException
	 */
	public NodeList getAuthNodeList(final String url, final String par) throws XPathException  {
		final Document document = xmlManager.getDocuments(xmlId);
		
		if (document == null) { throw new IllegalStateException("XML 도큐먼트가 NULL 입니다."); }
		
		final XPath xpath = XPathFactory.newInstance().newXPath();
		final NodeList nodeList = (NodeList)xpath.evaluate(SID_XPATH.replaceAll("\\{sid\\}", url).replaceAll("\\{par\\}", par), document, XPathConstants.NODESET);		
		
		return nodeList;
	}
	
	/**
	 * url에 대한 권한(job type (R,I,U,D))을  반환.
	 * url과 매칭되는 node list에 대해서 loop를 수행하며 Set를 확인한다. 내부적으로 getAuth(NodeList ...)를 호출한다.
	 * @param url
	 * @param roleName
	 * @param groupName
	 * @param par
	 * @param hasSession
	 * @return url에 대한 권한 (job type (R,I,U,D))을 반환
	 * @throws XPathException 
	 */
	public Set<String> getAuth(final String url, final String[] roleName, final String[] groupName, final String par, final boolean hasSession, final AbstractValidateMethodHolder method, final String remoteIp) throws XPathException {
		final Set<String> result = getAuth(getAuthNodeList(url, par), roleName, groupName, hasSession, method, remoteIp);
		return result;
	}
	
	/**
	 * url에 대한 권한(job type (R,I,U,D))을  반환<br/>
	 * url과 매칭되는 nodeList에 대해서 loop를 수행한다. loop 중 확인된 Set 중 가장 많은 항목를 포함하는 Set를 반환한다.
	 * (node list의 갯수는 일반적으로 1건, 예외적으로 n건이다.
	 * n건인 경우를 예를 들면 A URL을 A' user group에 대하여 읽기 허용하고, B' user group에 대하여 쓰기까지 허용하는 경우 URL에 대한 정의는 2건이다.)
	 * loop 수행 중 parameter로 넘겨진 role group, user group명에 해당하는 권한 정의를 set 형식으로 반환한다.
	 * nodeList를 
	 * @param nodeList url이 포함된 권한정의 node list 
	 * @param roleName 권한그룹명
	 * @param groupName 사용자그룹명
	 * @param par 동일한 url에 대하여 다른 권한정의가 필요할때, 호출 url에 포함될 parameter (parameter명은 _par이다.) 
	 * @param hasSession 현재 사용자가 session변수를 가지고 있는지 여부
	 * @param method 추가적으로 수행될 method. 이 매개변수의 수행결과가 false를 반환할 경우 반환되는 빈 Set이 반환된다. 
	 * @return
	 * @throws XPathException
	 */
	public Set<String> getAuth(final NodeList nodeList, final String[] roleName, final String[] groupName, final boolean hasSession, final AbstractValidateMethodHolder method, final String remoteIp) throws XPathException { 
		Set<String> set = new HashSet<String>();
		
		if (nodeList.getLength() == 0) {
			// node list가 0인 경우는 검증 대상 URL이 아니므로 무조건 모든 권한을 준다.
			set.addAll(getDefaultMethod());
		} else {
			boolean valid =	true;
			
			if (method != null) { valid = method.run(); }
			
			// 모든 loop를 수행하여야 한다. 반환받은 권한이 이전의 권한보다 클(이전 권한을 포함) 경우도 고려하여야 함.  
			for (int i = 0; valid && i < nodeList.getLength(); i++) {
				Set<String> bufSet = getMethod(nodeList.item(i), roleName, groupName, hasSession, remoteIp);
				logger.debug("bufSet:" + bufSet);
				set.addAll(bufSet);
			}
		}

		logger.debug("result set:" + set);
		return set;
	}

}